<?php

///////////////////////////////////////////////////////////////////////////////
/**
 * This file contains the abstract BaseConnector class.
 *
 * System requirements:
 * <ul>
 * <li>PHP 5</li>
 * </ul>
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * The Connector library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * {@link http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License}
 * for more details.
 *
 * @author Per Egil Roksvaag
 * @copyright 2009 Per Egil Roksvaag
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @package connector
 * @version 2.0.0
 */

///////////////////////////////////////////////////////////////////////////////
/**
 * Include the {@link IConnector} interface.
 */
require_once("IConnector.php");

/**
 * Include the {@link ConnectorException} class.
 */
require_once("ConnectorException.php");

/**
 * Include the {@link TypeValidator} class and the global function
 * {@link is_date()}.
 */
require_once("TypeValidator.php");

///////////////////////////////////////////////////////////////////////////////
/**
 * The BaseConnector implements common tasks for all IConnector
 * classes in this package.
 *
 * You can't create an instance of this class (it's abstract).
 * <b>Extend</b> it to create <b>new IConnector classes</b>,
 * e.g. for PHP database extensions not contained in this package.
 *
 * @tutorial connector.pkg#extend
 * @package connector
 */
abstract class BaseConnector implements IConnector
{
	///////////////////////////////////////////////////////////////////////////
	/**
	 * @var resource The PHP database link identifier.
	 */

	protected $conn;

	///////////////////////////////////////////////////////////////////////////
	/**
	 * @var array An associated array of cached query results.
	 */

	protected $cache = array();

	///////////////////////////////////////////////////////////////////////////
	/**
	 * @var array Default values for all global options.
	 * @tutorial connector.pkg#options.element
	 */

	protected $options = array
	(
		self::CHAR_APPLICATION => "UTF-8",
		self::CHAR_DATABASE => null,
		self::LOG_DEBUG => false,
		self::LOG_ERROR => true,
		self::THROW_EXCEPTION => true,
		self::PARAM_CAST_NUMERIC => true,
		self::PARAM_NAMED => false,
		self::PARAM_PREFIX => "?",
		self::PARAM_QUERIES => false,
		self::PARAM_STRIP_TAGS => false,
		self::PARAM_STRIP_MAGIC => false,
		self::PARAM_TRIM => false,
		self::PARAM_EMPTY_ARRAY_VALUE => "NULL",
		self::RESULT_CACHE => false
	//	self::RESULT_LENGTH only allowed as a local select() option.
	//	self::RESULT_OFFSET only allowed as a local select() option.
	//	self::RESULT_KEY_FIELD only allowed as a local select() option.
	);

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Call this method from descendent constructors to set global options.
	 *
	 * You can't create an instance of this class (it's abstract).
	 * @param array $options An associative array of options, see the {@tutorial connector.pkg#options.element}.
	 * @return BaseConnector
	 */

	public function BaseConnector($options = array())
	{
		$this->options[self::PARAM_STRIP_MAGIC] = (bool)get_magic_quotes_gpc();
		$this->setOptions($options);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Begins a transaction on the current connection.
	 * The current transaction includes all statements on the connection that
	 * were executed after the call to transaction() and before any calls
	 * to rollback() or commit().
	 *
	 * @see IConnector::transaction().
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @return bool true if the transaction was successfully begun, false otherwise.
	 */
	
	public function transaction($options = array())
	{
		return true;
	}
	
	///////////////////////////////////////////////////////////////////////////
	/**
	 * Commits the current transaction on the current connection.
	 * The current transaction includes all statements on the connection that
	 * were executed after the call to transaction() and before any calls
	 * to rollback() or commit().
	 *
	 * @see IConnector::commit().
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @return bool true if the transaction was successfully committed, false otherwise.
	 */
	
	public function commit($options = array())
	{
		return true;
	}
	
	///////////////////////////////////////////////////////////////////////////
	/**
	 * Rolls back the current transaction on the current connection. 
	 * The current transaction includes all statements on the connection that
	 * were executed after the call to transaction() and before any calls
	 * to rollback() or commit().
	 *
	 * @see IConnector::rollback().
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @return bool true if the transaction was successfully rolled back, false otherwise.
	 */
	
	public function rollback($options = array())
	{
		return true;
	}
	
	///////////////////////////////////////////////////////////////////////////
	/**
	 * Get the database link identifier used by the IConnector instance.
	 * @return resource A PHP database link identifier.
	 */

	public function getLink()
	{
		return $this->conn;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Get an array of all global options or the value of the global
	 * option defined in the optional $element parameter.
	 *
	 * @param string $element An option constant, see the {@tutorial connector.pkg#options.element}.
	 * @return mixed All options as an associated array or the value of a single option.
	 */

	public function getOptions($element = null)
	{
		return $element ? $this->lookup($element) : $this->options;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Set one or more global options.
	 * Some options are only valid as constructor or function parameters.
	 *
	 * @param mixed $options Multiple options as an associated array or the value of a single option.
	 * @param string $element An option constant, see the {@tutorial connector.pkg#options.element}.
	 * @return mixed The previous option value(s).
	 */

	public function setOptions($options, $element = null)
	{
		$result = $this->getOptions($element);

		if($element)
		{
			if(in_array($element, array_keys($this->options)))
			{
				$this->options[$element] = $options;
			}
		}
		else if(is_array($options))
		{
			foreach($options as $element => $value)
			{
				$this->setOptions($value, $element);
			}
		}
		return $result;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Calculate the md5 hash value of the query string and - if necessary -
	 * the parameters of a parameterized query.
	 *
	 * @param string $query The modified SQL query string after binding.
	 * @param array $stack An array of parameters to be used in a parameterized query.
	 * @param array $options An associated array of options.
	 * @return string The md5 hash value.
	 */

	protected function getHash($query, $stack = array(), $options = array())
	{
		return $stack && $this->lookup(self::PARAM_QUERIES)
			? md5($query.serialize($stack))
			: md5($query);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Check if a maching query result exists in the result cache.
	 *
	 * If true, set the <var>$table</var> argument to the cached query result,
	 * otherwise <var>$table</var> is set to an empty array.
	 *
	 * @param string $hash A hash value of a query, see {@link getHash()}.
	 * @param array &$table If found, return the cached query result, otherwise an empty array.
	 * @param array $options An associated array of options.
	 * @return bool true if a maching query result is found, false otherwise.
	 */

	protected function getCache($hash, &$table, $options = array())
	{
		if($this->lookup(self::RESULT_CACHE, $options))
		{
			if(array_key_exists($hash, $this->cache))
			{
				$table = $this->cache[$hash];
				return true;
			}
		}
		$table = array();
		return false;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Add a new query result to the result cache.
	 *
	 * @param string $hash A hash value of a query, see {@link getHash()}.
	 * @param array $table The query result to cache.
	 * @param array $options An associated array of options.
	 * @return int The number of cached query results.
	 */

	protected function setCache($hash, $table, $options = array())
	{
		if($this->lookup(self::RESULT_CACHE, $options))
		{
			$this->cache[$hash] = $table;
		}
		return count($this->cache);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Get an option value. Any options in the local $options array
	 * overrides the global options.
	 *
	 * @param string $element An option constant, see the {@tutorial connector.pkg#options.element}.
	 * @param array $options An associated array of options.
	 * @param mixed $default The default value to return if no global or local options are set. 
	 * @return mixed The value of a single option.
	 */

	protected function lookup($element, $options = array(), $default = null)
	{
		if($options && array_key_exists($element, $options))
		{
			return $options[$element];
		}
		else if(array_key_exists($element, $this->options))
		{
			return $this->options[$element];
		}
		else
		{
			return $default;
		}
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Replace the query placeholders with matching parameter values or - if
	 * parameterized queries are enabled - the database specific placeholder.
	 *
	 * Parameter values are converted, stripped, escaped, encoded and quoted
	 * according to the option values. The $stack returns an array of values
	 * for each placeholder to be used in parameterized queries.
	 *
	 * @param string $query A SQL query string.
	 * @param array $param An associated array of values to replace the $query placeholders.
	 * @param array &$stack Returns an array of values for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string The modified SQL query.
	 */

	protected function bind($query, $param, &$stack = array(), $options = array())
	{
		$result = "";
		$pattern = "/\B:(\w+)/";
		$query = trim($query);
		$split = preg_split($pattern, $query);
		$count = preg_match_all($pattern, $query, $matches, PREG_SET_ORDER);

		for($i = 0; $i < $count; $i++)
		{
			list($match, $name) = $matches[$i];
			$sql = $this->cast($param[$name], $name, $stack, $options);
			$result.= $split[$i];
			$result.= $sql !== null ? $sql : $match;
		}
		$result.= end($split);
		return $result;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Push a parameter on the stack and return a query placeholder.
	 *
	 * @param mixed $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Push parameters on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL placeholder.
	 */

	protected function push($value, $name, &$stack, $options = array())
	{
		if($this->lookup(self::PARAM_NAMED, $options))
		{
			$prefix = $this->lookup(self::PARAM_PREFIX, $options);
			$stack[$name] = $value;
			return $prefix.$name;
		}
		else
		{
			$prefix = $this->lookup(self::PARAM_PREFIX, $options);
			$stack[] = $value;
			return $prefix;
		}
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast a parameter to a valid SQL string or placeholder. The parameter is
	 * also pushed on the $stack - and if necessary converted - to be used in
	 * parameterized queries.
	 *
	 * @param mixed $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Push parameters on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function cast($value, $name, &$stack, $options = array())
	{
		if(is_null($value))		return $this->castNull($value, $name, $stack, $options);
		if(is_bool($value))		return $this->castBool($value, $name, $stack, $options);
		if(is_int($value))		return $this->castInt($value, $name, $stack, $options);
		if(is_float($value))	return $this->castFloat($value, $name, $stack, $options);
		if(is_date($value))		return $this->castDate($value, $name, $stack, $options);
		if(is_numeric($value))	return $this->castNumeric($value, $name, $stack, $options);
		if(is_string($value))	return $this->castString($value, $name, $stack, $options);
		if(is_object($value))	return $this->castObject($value, $name, $stack, $options);
		if(is_array($value))	return $this->castArray($value, $name, $stack, $options);
		return null;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast null to a valid SQL string.
	 *
	 * @param null $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Push parameters on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string.
	 */

	protected function castNull($value, $name, &$stack, $options = array())
	{
		return "NULL";
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast a boolean parameter to a valid SQL string or placeholder.
	 * The parameter is also pushed on the $stack and converted
	 * to be used in parameterized queries.
	 *
	 * @param bool $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	///////////////////////////////////////////////////////////////////////////

	protected function castBool($value, $name, &$stack, $options = array())
	{
		return $this->lookup(self::PARAM_QUERIES, $options)
			? $this->push((int)$value, $name, $stack, $options)
			: (string)(int)$value;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast an integer parameter to a valid SQL string or placeholder.
	 * The parameter is also pushed on the $stack to be used in parameterized
	 * queries.
	 *
	 * @param int $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castInt($value, $name, &$stack, $options = array())
	{
		return $this->lookup(self::PARAM_QUERIES, $options)
			? $this->push($value, $name, $stack, $options)
			: (string)$value;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast a float parameter to a valid SQL string or placeholder.
	 * The parameter is also pushed on the $stack to be used in parameterized
	 * queries.
	 *
	 * @param float $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castFloat($value, $name, &$stack, $options = array())
	{
		return $this->lookup(self::PARAM_QUERIES, $options)
			? $this->push($value, $name, $stack, $options)
			: (string)$value;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast a DateTime parameter to a valid SQL string or placeholder.
	 * The parameter is also pushed on the $stack to be used in parameterized
	 * queries.
	 *
	 * @param DateTime $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castDate($value, $name, &$stack, $options = array())
	{
		if($this->lookup(self::PARAM_QUERIES, $options))
		{
			return $this->push($value, $name, $stack, $options);
		}
		else
		{
			$sql = date_format($value, "Y-m-d H:i:s");
			return $this->strQuote($sql, $options);
		}
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast a numeric string parameter to a valid SQL string or placeholder.
	 * The parameter is also pushed on the $stack and converted to be used in
	 * parameterized queries.
	 *
	 * @param string $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castNumeric($value, $name, &$stack, $options = array())
	{
		if($this->lookup(self::PARAM_CAST_NUMERIC, $options))
		{
			$int = (int)$value;
			$float = (float)$value;
			
			if((string)$int === $value)
			{
				return $this->castInt($int, $name, $stack, $options);
			}
			if((string)$float === $value)
			{
				return $this->castFloat($float, $name, $stack, $options);
			}
		}
		return $this->castString($value, $name, $stack, $options);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Cast a string parameter to a valid SQL string or placeholder.
	 * The parameter is also pushed on the $stack and modified to be used in
	 * parameterized queries.
	 *
	 * String values are converted, stripped, escaped, encoded and quoted
	 * according to the option values.
	 *
	 * @param string $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castString($value, $name, &$stack, $options = array())
	{
		$value = $this->strStrip($value, $options);

		if($this->lookup(self::PARAM_QUERIES, $options))
		{
			$value = $this->strEncode($value, $options);
			return $this->push($value, $name, $stack, $options);
		}
		else
		{
			$value = $this->strEscape($value, $options);
			$value = $this->strEncode($value, $options);
			return $this->strQuote($value, $options);
		}
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * With the exception of DataTime values, objects are not supported as
	 * parameters. Override this method to serialize your objects as
	 * SQL query strings.
	 *
	 * @param object $value The parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castObject($value, $name, &$stack, $options = array())
	{
		return null;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Arrays are converted to a comma separated list of values.
	 *
	 * Strings are escaped and quoted. No placeholders are returend.
	 * Any arrays within the array are ignored. If the array is empty,
	 * the string "NULL" is returned.
	 *
	 * @param array $list An array of parameter to replace the query placeholder.
	 * @param string $name The name of the query placeholder.
	 * @param array &$stack Parameters are pushed on this array for use in parameterized queries.
	 * @param array $options An associated array of options.
	 * @return string A valid SQL string or placeholder.
	 */

	protected function castArray($list, $name, &$stack, $options = array())
	{
		$sql = array();
		$options[self::PARAM_QUERIES] = false;

		foreach($list as $value)
		{
			if(is_null($value))			$sql[] = $this->castNull($value, $name, $stack, $options);
			else if(is_bool($value))	$sql[] = $this->castBool($value, $name, $stack, $options);
			else if(is_int($value))		$sql[] = $this->castInt($value, $name, $stack, $options);
			else if(is_float($value))	$sql[] = $this->castFloat($value, $name, $stack, $options);
			else if(is_date($value))	$sql[] = $this->castDate($value, $name, $stack, $options);
			else if(is_numeric($value))	$sql[] = $this->castNumeric($value, $name, $stack, $options);
			else if(is_string($value))	$sql[] = $this->castString($value, $name, $stack, $options);
			else if(is_object($value))	$sql[] = $this->castObject($value, $name, $stack, $options);
		}
		return count($sql)
			? implode(",", $sql)
			: $this->lookup(self::PARAM_EMPTY_ARRAY_VALUE, $options);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Strip spare or insecure parts from a string parameter
	 * according to the current options.
	 *
	 * @param string $value The string that will be stripped.
	 * @param array $options An associated array of options.
	 * @return string The stripped string.
	 */

	protected function strStrip($value, $options = array())
	{
		if($this->lookup(self::PARAM_STRIP_MAGIC, $options))
		{
			$value = stripslashes($value);
		}
		if($this->lookup(self::PARAM_STRIP_TAGS, $options))
		{
			$value = strip_tags($value);
		}
		if($this->lookup(self::PARAM_TRIM, $options))
		{
			$value = trim($value);
		}
		return $value;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Escape special characters in a string parameter.
	 *
	 * @param string $value The string to be escaped.
	 * @param array $options An associated array of options.
	 * @return string The escaped string.
	 */

	protected function strEscape($value, $options = array())
	{
		return str_replace("'", "''", $value);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Add surrounding quotes to a string parameter.
	 *
	 * @param string $value The string to be quoted.
	 * @param array $options An associated array of options.
	 * @return string The quoted string.
	 */

	protected function strQuote($value, $options = array())
	{
		return "'".$value."'";
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Converts character encoding of string parameters to the character
	 * set of the database according to the current options.
	 *
	 * @param string $value The string that will be converted.
	 * @param array $options An associated array of options.
	 * @return string The converted string.
	 */

	protected function strEncode($value, $options = array())
	{
		$db = $this->lookup(self::CHAR_DATABASE, $options);
		$app = $this->lookup(self::CHAR_APPLICATION, $options);

		if($db && $app && $db != $app)
		{
			if(function_exists("mb_convert_variables"))
			{
				mb_convert_variables($db, $app, $value);
			}
			else if(function_exists("iconv"))
			{
				if(is_string($value))
				{
					return iconv($app, $db, $value);
				}
				else if(is_array($value))
				{
					foreach($value as $key => $cell)
					{
						if(is_string($cell))
						{
							$value[$key] = iconv($app, $db, $cell);
						}
					}
				}
			}
		}
		return $value;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Converts character encoding of from the character
	 * set of the database according to the current options.
	 *
	 * @param string $value The string that will be converted.
	 * @param array $options An associated array of options.
	 * @return string The converted string.
	 */

	protected function strDecode($value, $options = array())
	{
		$db = $this->lookup(self::CHAR_DATABASE, $options);
		$app = $this->lookup(self::CHAR_APPLICATION, $options);

		if($db && $app && $db != $app)
		{
			if(function_exists("mb_convert_variables"))
			{
				mb_convert_variables($app, $db, $value);
			}
			else if(function_exists("iconv"))
			{
				if(is_string($value))
				{
					return iconv($db, $app, $value);
				}
				else if(is_array($value))
				{
					foreach($value as $key => $cell)
					{
						if(is_string($cell))
						{
							$value[$key] = iconv($db, $app, $cell);
						}
					}
				}
			}
		}
		return $value;
	}

	///////////////////////////////////////////////////////////////////////////
}

?>